# /**
# * Copyright (c) 2022 Beijing Jiaotong University
# * PhotLab is licensed under [Open Source License].
# * You can use this software according to the terms and conditions of the [Open Source License].
# *
# * You may obtain a copy of [Open Source License] at: [https://open.source.license/]
# *
# * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# *
# * See the [Open Source License] for more details.
# */
# /**
# * Author: Shihao Xie
# * Created: Apr. 17, 2023
# * Supported by: National Key Research and Development Program of China
# */

from functools import wraps

from phot_server.core.exception import IdDuplicationException, ResourceNotFoundError, ParameterValueError
from phot_server.rpc.graph_data_type.ttypes import PhotGraph, PhotParameter, PhotComponent, PhotGraphLink
from phot_server.utils.singleton import singleton


def monitor_graph_change():
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            GraphRepository().update[args[1]] = True
            return function(*args, **kwargs)

        return wrapper

    return decorator


# temporarily storage graph data
@singleton
class GraphRepository:

    def __init__(self):
        self.__graphs = {}
        self.update = {}

    def add_graph(self, graph: PhotGraph):
        """
        This method is to add a graph to container.
        :param graph: graph object
        :return: PhotGraph object that be added
        """
        if self.has_graph(graph.graphId):
            raise IdDuplicationException(PhotGraph.__name__, graph.graphId)
        self.__graphs[graph.graphId] = graph
        self.update[graph.graphId] = False
        return graph

    def get_graph(self, graph_id) -> PhotGraph:
        """
        This method is to get a graph in graph container
        :param graph_id: the name of graph in graph container
        :return: PhotGraph object that contain graph data
        """
        graph = self.__graphs.get(graph_id, None)
        if graph is None:
            raise ResourceNotFoundError(PhotGraph.__name__, graph_id)
        else:
            return graph

    def get_graphs(self) -> dict:
        """
        This method is to get a graph in graph container
        :param graph_id: the name of graph in graph container
        :return: PhotGraph object that contain graph data
        """
        return self.__graphs

    def has_graph(self, graph_id) -> bool:
        return True if self.__graphs.get(graph_id) is not None else False

    def delete_graph(self, graph_id):
        """
        This method is to delete a graph in graph container by graph name
        :param graph_id: the name of graph in graph container
        :return: PhotGraph object that being deleted
        """
        if self.has_graph(graph_id):
            self.__graphs.pop(graph_id)
            self.update.pop(graph_id, None)
        else:
            raise ResourceNotFoundError(PhotGraph.__name__, graph_id)

    def set_graph(self, graph_id, graph):
        """
        This method is to get a graph in graph container
        :param graph: new graph
        :param graph_id: the name of graph in graph container
        :return: PhotGraph object that contain graph data
        """
        self.delete_graph(graph_id)
        return self.add_graph(graph)

    def rename_graph(self, graph_id, new_name):
        graph = self.get_graph(graph_id)
        graph.name = new_name
        self.update.update({graph_id: True})

    # master information
    @monitor_graph_change()
    def update_graph_attributes(self, graph_id, updated_parameters: dict):
        """
        This method is to alter graph information in xml <master> element.
        :param graph_id: the name of graph in graph container
        :param updated_parameters:
        :return: PhotMaster object that contain graph master information
        """
        graph = self.get_graph(graph_id)
        for key, value in updated_parameters.items():
            if hasattr(graph, key) is False:
                raise ResourceNotFoundError(
                    message="Could not find attribute '{}' in graph '{}'".format(key, graph_id))
            elif isinstance(value, (list, dict)):
                raise ParameterValueError("Could not update parameters that its type is '{}'.".format(type(value)))
            else:
                setattr(graph, key, value)
        graph.graphId = graph_id

    # server_config parameter
    def get_graph_parameter(self, graph_id, parameter_name, raise_error=True):
        """
        This method is to get a server_config parameter information in a graph
        :param graph_id: the name of graph in graph container
        :param parameter_name: the name of server_config parameter in graph
        :param raise_error: return None or raise error if it has no match graph parameter
        :return: PhotParameter object that conatin parameter data
        """
        graph = self.get_graph(graph_id)
        for category, parameters in graph.graphParameters.items():
            for parameter in parameters:
                if parameter.name == parameter_name:
                    parameter.category = category
                    return parameter
        if raise_error:
            raise ResourceNotFoundError(
                message="Graph Parameter {} is not exist in graph {}".format(parameter_name, graph_id))
        return None

    def has_graph_parameter(self, graph_id, parameter_name):
        return True if self.get_graph_parameter(graph_id, parameter_name, False) is not None else False

    def get_graph_parameters(self, graph_id) -> dict:
        """
        This method is to get all server_config parameters information in a graph
        :param graph_id: the name of graph in graph container
        :return: PhotParameter object map
        """
        return self.get_graph(graph_id).graphParameters

    @monitor_graph_change()
    def alter_graph_parameter(self, graph_id, parameter_name, parameter):
        """
        This method is to alter a server_config parameter information in a graph
        :param graph_id: the name of graph in graph container
        :param parameter_name: the name of server_config parameter in graph
        :param parameter: PhotParameter object that conatin parameter data
        :return: PhotParameter object that conatin new parameter data
        """
        graph = self.get_graph(graph_id)
        graph_parameter = self.get_graph_parameter(graph_id, parameter_name)
        graph.graphParameters[graph_parameter.category].remove(graph_parameter)
        self.add_graph_parameter(graph_id, parameter)

    @monitor_graph_change()
    def alter_graph_parameters(self, graph_id, parameter_list):
        """
        This method is to alter some server_config parameters in a graph
        :param graph_id: the name of graph in graph container
        :param parameter_list: the server_config parameter list alter to
        :return: new PhotParameter object map
        """
        self.get_graph(graph_id).graphParameters = parameter_list

    @monitor_graph_change()
    def add_graph_parameter(self, graph_id, parameter: PhotParameter):
        """
        This method is to add a server_config parameter in a graph
        :param graph_id: the name of graph in graph container
        :param parameter: the server_config parameter will add to graph
        :return: PhotParameter object of new server_config parameter
        """
        if self.has_graph_parameter(graph_id, parameter.name) is False:
            category = parameter.category if parameter.category is not None else ""
            if parameter.category not in self.get_graph(graph_id).graphParameters.keys():
                self.get_graph(graph_id).graphParameters[category] = []
            self.get_graph(graph_id).graphParameters[category].append(parameter)
            return parameter
        else:
            raise IdDuplicationException(PhotParameter.__name__, parameter.name, PhotGraph.__name__, graph_id)

    @monitor_graph_change()
    def delete_graph_parameter(self, graph_id, parameter_name):
        """
        This method is to delete a server_config parameter in a graph
        :param graph_id: the name of graph in graph container
        :param parameter_name: the server_config parameter to be deleted
        :return: PhotParameter object of being deleted server_config parameter
        """
        graph = self.get_graph(graph_id)
        parameter = self.get_graph_parameter(graph_id, parameter_name)
        graph.graphParameters[parameter.category].remove(parameter)

    # component
    @monitor_graph_change()
    def add_component(self, graph_id, component: PhotComponent):
        """
        This method is to add a component in a graph
        :param graph_id: the name of graph in graph container
        :param component: PhotComponent object to be added
        :return: PhotComponent object of being added one
        """
        if self.has_component(graph_id, component.componentId):
            raise IdDuplicationException(PhotComponent.__name__, component.componentId, PhotGraph.__name__, graph_id)
        self.get_graph(graph_id).components.append(component)
        return component

    def get_component(self, graph_id, component_id, raise_error=True):
        """
        This method is to get a component in a graph
        :param graph_id: the name of graph in graph container
        :param component_id: the id of component
        :param raise_error: return None or raise error if it has no match component
        :return: PhotComponent object
        """
        graph = self.get_graph(graph_id)
        for component in graph.components:
            if component.componentId == component_id:
                return component
        if raise_error:
            raise ResourceNotFoundError(PhotComponent.__name__, component_id, PhotGraph.__name__, graph_id)
        return None

    def has_component(self, graph_id, component_id):
        return True if self.get_component(graph_id, component_id, False) is not None else False

    def get_components(self, graph_id) -> list:
        """
        This method is to get all components in a graph
        :param graph_id: the name of graph in graph container
        :return: PhotComponent object map
        """
        return self.get_graph(graph_id).components

    @monitor_graph_change()
    def alter_component(self, graph_id, component):
        """
        This method is to alter a component in a graph
        :param graph_id: the name of graph in graph container
        :param component: PhotComponent object to alter
        :return: new PhotComponent object
        """
        graph = self.get_graph(graph_id)
        self.delete_component(graph_id, component.componentId)
        graph.components.append(component)

    @monitor_graph_change()
    def delete_component(self, graph_id, component_id):
        """
        This method is to delete a component in a graph
        :param graph_id: the name of graph in graph container
        :param component_id: the id of component
        :return: PhotComponent object to be deleted
        """
        graph = self.get_graph(graph_id)
        target = self.get_component(graph_id, component_id)
        graph.components.remove(target)

    # link
    def get_link(self, graph_id, link_id, raise_error=True):
        graph = self.get_graph(graph_id)
        for link in graph.links:
            if link.linkId == link_id:
                return link
        if raise_error:
            raise ResourceNotFoundError(PhotGraphLink.__name__, link_id, PhotGraph.__name__, graph_id)
        return None

    def has_link(self, graph_id, link_id):
        return True if self.get_link(graph_id, link_id, False) is not None else False

    def get_links(self, graph_id):
        """
        This method is to get all links in a graph
        :param graph_id: the name of graph in graph container
        :return: PhotLink list
        """
        return self.get_graph(graph_id).links

    @monitor_graph_change()
    def add_link(self, graph_id, link: PhotGraphLink):
        """
        This method is to add a link in a graph
        :param graph_id: the name of graph in graph container
        :param link: PhotLink object that contain link data
        :return: PhotLink object been added
        """
        if self.has_link(graph_id, link.linkId) is False:
            self.get_graph(graph_id).links.append(link)
            return link
        else:
            raise IdDuplicationException(PhotGraphLink.__name__, link.linkId, PhotGraph.__name__, graph_id)

    @monitor_graph_change()
    def alter_link(self, graph_id, link: PhotGraphLink):
        """
        This method is to alter all links in a graph
        :param link: link alter to
        :param graph_id: the name of graph in graph container
        :return: new link list
        """
        self.delete_link(graph_id, link.linkId)
        return self.add_link(graph_id, link)

    @monitor_graph_change()
    def alter_links(self, graph_id, links: list):
        """
        This method is to alter all links in a graph
        :param graph_id: the name of graph in graph container
        :param links: link list
        :return: new link list
        """
        self.get_graph(graph_id).links = links

    @monitor_graph_change()
    def delete_link(self, graph_id, link_id):
        """
        This method is to delete a link in a graph
        :param graph_id: the name of graph in graph container
        :param link_id: link to be deleted
        :return: the link been deleted
        """
        graph = self.get_graph(graph_id)
        target = self.get_link(graph_id, link_id)
        graph.links.remove(target)
